package com.asdf.application;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import com.asdf.db.DataBase;
import com.asdf.db.DataBase.DataSource;
import com.asdf.db.operations.airport.InsertAirport;
import com.asdf.db.operations.airport.LocateAirport;
import com.asdf.db.operations.companyclub.GetCompanyClubOp;
import com.asdf.db.operations.companyclub.InsertCompanyClub;
import com.asdf.db.operations.stat.InsertStat;
import com.asdf.db.operations.user.GetUserByUsernameOp;
import com.asdf.db.operations.user.InsertUser;
import com.asdf.db.results.ResultStatus;
import com.asdf.db.results.airport.AirportResult;
import com.asdf.db.results.companyclub.CompanyClubResult;
import com.asdf.db.results.stat.StatResult;
import com.asdf.db.results.user.UserResult;
import com.asdf.entities.Airport;
import com.asdf.entities.CompanyClub;
import com.asdf.entities.Connection;
import com.asdf.entities.CreditCard;
import com.asdf.external.DBUsageDetails;
import com.asdf.external.DBUserDetails;
import com.asdf.external.INetserviceSimulator;
import com.asdf.external.IVSDT;
import com.asdf.external.LogWriter;
import com.asdf.external.stubs.AviationCompanyClub;
import com.asdf.external.stubs.CreditCardCompany;
import com.asdf.external.stubs.VSDT;

public class NetServiceSimulator implements INetserviceSimulator
{
	/*
	 * Variable which describes the current location in simulation
	 * that means whenever we want to change it should be affected by
	 * it, need to pay attention it could be not initialized.
	 */
	private Airport currentLocation = null;
	
	// (Airport_id, (Username, Connection))
	Map<String, Map<String,Connection>> worldWideConns = new HashMap<String, Map<String,Connection>>();
	
	private boolean validateXMLFile( String xsdFileName, String xmlFileName)
	{
		try
		{
			// define the type of schema - we use W3C:
			String schemaLang = "http://www.w3.org/2001/XMLSchema";

			// get validation driver:
			SchemaFactory factory = SchemaFactory.newInstance( schemaLang);

			// create schema by reading it from an XSD file:
			Schema schema = factory.newSchema( new StreamSource( xsdFileName));
			Validator validator = schema.newValidator( );

			// at last perform validation:
			validator.validate( new StreamSource( xmlFileName));
		}
		catch ( SAXException ex )
		{
			LogWriter.getInstance( ).reportError( ex.getMessage( ));
			return false;
		}
		catch ( Exception ex )
		{
			LogWriter.getInstance( ).reportError( ex.getMessage( ));
			return false;
		}
		return true;
	}

	public void loadClubs( String clubsFile)
	{
		try
		{
			Document document = getRootedElement( clubsFile);
			
			extractClubData( document);
		}
		catch ( Exception e )
		{
			LogWriter.getInstance( ).reportError( e.getMessage( ));
		}
	}

	@SuppressWarnings("unchecked")
	private void extractClubData( Document document)
	{
		NodeList companiesList = document.getElementsByTagName( Constants.COMPANY_TAG_NAME);
		
		for ( int index = 0; index < companiesList.getLength( ); ++index)
		{
			Element club = ( Element)companiesList.item( index);
			
			String id       = club.getAttributes( )
								.getNamedItem( Constants.COMPANY_ID_TAG_NAME).getNodeValue( ).trim( );
			String name     = club.
								getElementsByTagName( Constants.COMPANY_NAME_TAG_NAME).item( 0)
									.getChildNodes( ).item( 0).getNodeValue( ).trim( );
			double discount = Double.parseDouble( club.
													getElementsByTagName( Constants.COMPANY_DISCOUNT_TAG_NAME)
														.item( 0).getChildNodes( ).item( 0).getNodeValue( ).trim( ));
			
			CompanyClub tmp = new CompanyClub( name, discount, id);
			InsertCompanyClub insOp = new InsertCompanyClub( ( List<CompanyClub> ) 
											DataBase.getInstance( )
												.getDataSource( DataSource.CompanyClubs) );
			insOp.setCompanyClub( tmp);
			DataBase.getInstance( ).Do( insOp);
		}
	}

	public void loadAirports( String airportsFile)
	{
		try
		{
			Document document = getRootedElement( airportsFile);
			
			extractAirportData( document);
		}
		catch ( Exception e )
		{
			LogWriter.getInstance( ).reportError( e.getMessage( ));
		}
	}

	private Document getRootedElement( String airportsFile)
			throws ParserConfigurationException, SAXException, IOException
	{
		File xmlFile = new File ( airportsFile);
		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance( );
		DocumentBuilder db = dbf.newDocumentBuilder( );
		
		Document document = db.parse( xmlFile);
		document.normalize( );
		return document;
	}

	@SuppressWarnings("unchecked")
	private void extractAirportData( Document document)
	{
		NodeList airportList = document.getElementsByTagName( Constants.AIRPORT_TAG_NAME);
		
		for ( int index = 0; index < airportList.getLength( ); ++index)
		{
			Element airport = ( Element)airportList.item( index);
			
			String id      = airport.getAttributes( )
								.getNamedItem( Constants.AIRPORT_ID_TAG_NAME).getNodeValue( ).trim( );
			String name    = airport.
								getElementsByTagName( Constants.AIRPORT_NAME_TAG_NAME).item( 0)
									.getChildNodes( ).item( 0).getNodeValue( ).trim( );
			String country = airport
								.getElementsByTagName( Constants.AIRPORT_COUNTRY_TAG_NAME)
									.item( 0).getChildNodes( ).item( 0).getNodeValue( ).trim( );
			String license = airport
								.getElementsByTagName( Constants.AIRPORT_LICENSE_TAG_NAME)
									.item( 0).getChildNodes( ).item( 0).getNodeValue( ).trim( );
			double rate    = Double.parseDouble( airport.
													getElementsByTagName( Constants.AIRPORT_RATE_TAG_NAME)
														.item( 0).getChildNodes( ).item( 0).getNodeValue( ).trim( ));
			// Convert time units for charge to $ per msec.
			Airport tmp = new Airport( id, country, license, name, rate/(60*1000));
			
			worldWideConns.put(id, new HashMap<String, Connection>());
			InsertAirport insOp = new InsertAirport( ( List<Airport> ) DataBase.getInstance( ).getDataSource( DataSource.AirportList) );
			insOp.setAirport( tmp);
			DataBase.getInstance( ).Do( insOp);
			this.currentLocation = tmp;
		}
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public boolean endConnection( String username, int chargeTime)
	{
		Map<String,Connection> airportConns =
			worldWideConns.get(currentLocation.getId());
		Connection conn = airportConns.remove(username);
		DBUsageDetails bill = null;
		
		if (conn == null)
		{
			LogWriter.getInstance( ).reportWarning("User " + username +
				" is not connected in airport " + currentLocation.getName() +
				"."); 
			return false;
		}

		// The bill should always be non-null. But it looks much more pretty
		// in this way.
		if ((bill = conn.endAndGetUsageDetails(chargeTime)) != null)
		{
			InsertStat op = new InsertStat((List<DBUsageDetails>)DataBase.
				getInstance().getDataSource(DataSource.StatList));
			StatResult res = null;
			
			op.setDBUsageDetails(bill);
			res = (StatResult)DataBase.getInstance().Do(op);
			// Memory lacks - we have nothing to do.
			if (res.getStatus() != ResultStatus.OK)
				return false;
		}
		
		return true;
	}

	@Override
	public boolean init( String airportsFile, String clubsFile)
	{
		if ( !validateXMLFile( Constants.XSD_FILES_NAMES[0], airportsFile) ||
			 !validateXMLFile( Constants.XSD_FILES_NAMES[1], clubsFile))
			return false;
		
		loadAirports( airportsFile);
		loadClubs( clubsFile);
		
		return true;
	}
	
	protected Connection _startConnection(String username, String password)
	{
		Connection conn = new Connection(currentLocation);
		Map<String,Connection> airportConns =
			worldWideConns.get(currentLocation.getId( ));
		
		if (!conn.setUser(username, password))
		{
			LogWriter.getInstance( ).reportWarning("Username " + username +
				" or password " + password + " is not correct.");
			return null;
		}
		
		if (airportConns.containsKey(username))
		{
			LogWriter.getInstance( ).reportWarning("Username " + username +
				" is already connected in airport" +
				currentLocation.getName( ) + ".");
			return null;
		}
		
		airportConns.put(username, conn);
		conn.start();
		return conn;
	}

	@Override
	public boolean startConnection( String username, String password,
			String ccNumber, String ccProvider, int ccExpirationMonth,
			int ccExpirationYear, int ccZIP)
	{
		VSDT vsdt = new VSDT();
		CreditCard card = null;
		Connection conn = null;
		
		card = new CreditCard(vsdt.decrypt(ccNumber), vsdt.decrypt(ccProvider),
				ccExpirationMonth, ccExpirationYear, ccZIP);
		if (!card.checkValidity())
		{
			LogWriter.getInstance( ).reportWarning("Credit card is invalid.");
			return false;
		}
		
		if (null == (conn = _startConnection(vsdt.decrypt(username),
			vsdt.decrypt(password))))
		{
			LogWriter.getInstance( ).reportWarning("Failed to start connection");
			return false;
		}
		
		conn.setCard(card);
		return true;
	}

	@Override
	public boolean startConnection( String username, String password)
	{
		VSDT vsdt = new VSDT();
		
		// check 100% discount
		if (!isFreeAccessUser( vsdt.decrypt( username))) {
			LogWriter.getInstance( ).reportWarning("User has no free access");
			return false;
		}
		
		if (_startConnection(vsdt.decrypt(username), vsdt.decrypt(password))
			== null)
		{
			LogWriter.getInstance( ).reportWarning("Failed to start connection.");
			return false;
		}
		
		return true;
	}

	@SuppressWarnings("unchecked")
	private String _subscribe( String id, String firstName, String lastName,
			String email, String address, boolean aviationClubMember,
			String aviationCompanyName, boolean getNewsLetters, String username)
	{
		IVSDT vsdt = new VSDT();
		DBUserDetails user = new DBUserDetails();
			
		user.setId(vsdt.decrypt(id));
		user.setFirstName(vsdt.decrypt(firstName));
		user.setLastName(vsdt.decrypt(lastName));
		user.setEmail(vsdt.decrypt(email));
		user.setAddress(vsdt.decrypt(address));
		user.setAviationClubMember(aviationClubMember);
		user.setAviationCompanyName(vsdt.decrypt(aviationCompanyName));
		user.setGetNewsLetters(getNewsLetters);
		user.setUsername(vsdt.decrypt(username));
		// Initial password is an email.
		user.setPassword(vsdt.decrypt(email));
		
		InsertUser op = new InsertUser((List<DBUserDetails>)DataBase.
				getInstance().getDataSource(DataSource.UserList));
		op.setUser( user);
		
		UserResult res = (UserResult)DataBase.getInstance().Do(op);
		if (res.getStatus() != ResultStatus.OK)
		{
			// User exists
			LogWriter.getInstance( ).reportWarning("Failed to add new user.");
			return null;
		}
		
		return user.getPassword();
	}
	
	@Override
	public String subscribe( String id, String firstName, String lastName,
			String email, String address, boolean aviationClubMember,
			String aviationCompanyName, boolean getNewsLetters,
			String username, String ccNumber, String ccProvider,
			int ccExpirationMonth, int ccExpirationYear, int ccZIP)
	{
		VSDT vsdt = new VSDT();
		CreditCardCompany ccc = new CreditCardCompany();


		// check for empty and null input strings
		for (String s : Arrays.asList(id, firstName, lastName, email, address,
						username, ccNumber, ccProvider, 
						aviationClubMember?aviationCompanyName:vsdt.encrypt("dummy")) )
		{
			if(s==null || vsdt.decrypt(s)==null || vsdt.decrypt(s).isEmpty())
			{
				LogWriter.getInstance( ).reportWarning("Field cannot be empty.");
				return null;
			}
		}
		
		// check club membership, if specified
		AviationCompanyClub aviacompany = new AviationCompanyClub();
		if (aviationClubMember && !aviacompany.checkValidity(vsdt.decrypt(id),
			vsdt.decrypt(aviationCompanyName)))
		{
			LogWriter.getInstance( ).reportWarning("Not a club member.");
			return null;
		}

		// check credit card validity
		if (!ccc.checkValidity(vsdt.decrypt(ccNumber),
			vsdt.decrypt(ccProvider), ccExpirationMonth, ccExpirationYear,
			ccZIP))
		{
			// Credit card is invalid.
			LogWriter.getInstance( ).reportWarning("Credit card is invalid.");
			return null;
		}
		
		return _subscribe(id, firstName, lastName, email, address,
			aviationClubMember, aviationCompanyName, getNewsLetters, username);
	}

	@Override
	public String subscribe( String id, String firstName, String lastName,
			String email, String address, boolean aviationClubMember,
			String aviationCompanyName, boolean getNewsLetters, String username)
	{
		VSDT vsdt = new VSDT();

		// check for empty and null input strings
		for (String s : Arrays.asList(id, firstName, lastName, email, address, username,
						aviationClubMember?aviationCompanyName:vsdt.encrypt("dummy")) )
			if(s==null || vsdt.decrypt(s)==null || vsdt.decrypt(s).isEmpty()) {
				LogWriter.getInstance( ).reportWarning("Field cannot be empty.");
				return null;
			}
		
		// check club membership
		AviationCompanyClub aviacompany = new AviationCompanyClub();
		if (!aviationClubMember || !aviacompany.checkValidity(vsdt.decrypt(id),
			vsdt.decrypt(aviationCompanyName)))
		{
			LogWriter.getInstance( ).reportWarning("Not a real club member.");
			return null;
		}

		if (!isFreeAccessClub(vsdt.decrypt(aviationCompanyName))) {
			LogWriter.getInstance( ).reportWarning("User's club has not 100% discount");
			return null;
		}

		return _subscribe(id, firstName, lastName, email, address,
				aviationClubMember, aviationCompanyName, getNewsLetters, username);
	}

	/** is aviation company club exist and has 100% discount? */
	@SuppressWarnings("unchecked")
	private boolean isFreeAccessClub(String club)
	{
		GetCompanyClubOp op = new GetCompanyClubOp(
				(List<CompanyClub>)DataBase.getInstance().getDataSource(DataSource.CompanyClubs));
		
		op.setCompanyName( club );
		CompanyClubResult res = (CompanyClubResult)DataBase.getInstance().Do(op);
		return res.getStatus() == ResultStatus.OK && (float)res.getCompany().getDiscount() == 1.0;
	}
	
	/** is a user has 100% from his aviation company club? */
	@SuppressWarnings("unchecked")
	private boolean isFreeAccessUser(String username)
	{
		GetUserByUsernameOp op = new GetUserByUsernameOp((List<DBUserDetails>)
				DataBase.getInstance().getDataSource(DataSource.UserList));
		
		op.setUsername(username);
		UserResult res = (UserResult)DataBase.getInstance().Do(op);
		if (res.getStatus() != ResultStatus.OK)
			return false;
		
		DBUserDetails user = res.getUser( );
		return user.getAviationClubMember() && isFreeAccessClub( user.getAviationCompanyName() );
	}

	/** Switch current airport.
	 * @param id - id of the airport you wanna switch to
	 */
	@SuppressWarnings("unchecked")
	@Override
	public void switchAirport( String id)
	{
		LocateAirport locate = new LocateAirport( ( List<Airport> ) DataBase.getInstance( ).
															getDataSource( DataSource.AirportList));
		locate.setAirportID( id);
		AirportResult result = ( AirportResult ) DataBase.getInstance( ).Do( locate);
		List<Airport> resultList = result.getResultSet( );
		if ( resultList.size( ) == 0)
		{
			LogWriter.getInstance( ).reportWarning( "Cannot switch, airport not found.");
			return;
		}
		currentLocation = resultList.get( 0);
	}
	
	/** Returns the duration of the connection of current selected airport for
	 * the received user.
	 * @param username - the username (not using VSDT)
	 * @return - duration time in second in successful case. If fails < 0.
	 */
	public int getConnectionTime(String username)
	{
		Connection conn = null;
		Map<String,Connection> airportConns =
			worldWideConns.get(currentLocation.getId());
		
		if ((conn = airportConns.get(username)) == null)
		{
			LogWriter.getInstance( ).reportWarning("There is no connection for"
				+ username + " in airport " + currentLocation);
			return -1;
		}
		
		return (int)conn.getDuration( );
	}

}
